﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.ServiceModel.Syndication;
using System.Xml;

namespace ServiceModel.Syndication.LEAP2A {
    /// <summary>
    /// Adds LEAP2A support to Atom feeds and provides error checking and feed repair
    /// </summary>
    public class LeapFeed : SyndicationFeed {
        private readonly LeapLinkManager linkManager;
        private readonly List<LeapFeedError> feedErrors = new List<LeapFeedError>();
        private readonly IDictionary<string, LeapItem> lookup;
        private IDictionary<string, string> namespaces;
        private string localNamespace, localSchemeNamespace;

        /// <summary>
        /// Gets the LEAP errors.
        /// </summary>
        /// <value>The leap feed errors.</value>
        public ICollection<LeapFeedError> LeapFeedErrors {
            get { return feedErrors; }
        }

        /// <summary>
        /// Gets a reference to the LEAP link manager.
        /// </summary>
        /// <value>The leap link manager.</value>
        public LeapLinkManager LeapLinkManager {
            get { return linkManager; }
        }

        /// <summary>
        /// Gets or sets the path of the extracted zip containing the LEAP2A feed.
        /// </summary>
        /// <value>The import base path.</value>
        public string ImportBasePath {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the namespace used for portfolio items, 
        /// usually the URL of the portfolio and a user identifier.
        /// </summary>
        /// <value>The portfolio namespace.</value>
        public string LocalNamespace {
            get {
                return localNamespace;
            }

            set {
                localNamespace = value;
                this.namespaces[LocalNamespacePrefix] = value;
            }
        }

        /// <summary>
        /// Gets or sets the namespace used for local schemes, 
        /// this includes custom categories and extension elements.
        /// </summary>
        /// <value>The local scheme namespace.</value>
        public string LocalSchemeNamespace {
            get { 
                return localSchemeNamespace; 
            }

            set {
                localSchemeNamespace = value;
                this.namespaces[LocalSchemeNamespacePrefix] = value;
            }
        }

        /// <summary>
        /// Gets the portfolio namespace prefix.
        /// </summary>
        /// <value>The portfolio namespace prefix.</value>
        public string LocalNamespacePrefix {
            get { return "portfolio"; }
        }

        /// <summary>
        /// Gets the local scheme namespace prefix.
        /// </summary>
        /// <value>The local scheme namespace prefix.</value>
        public string LocalSchemeNamespacePrefix {
            get { return "localschemes"; }
        }

        public string LeapPredicatesNamespace {
            get { 
                string ns = namespaces.FirstOrDefault(n => LeapPredicateValues.ContainsNamespace(n.Value)).Value; 
                
                if (string.IsNullOrEmpty(ns)) {
                    ns = LeapPredicateValues.Namespaces[Version];
                }

                return ns;
            }
        }

        public string LeapPredicatesNamespacePrefix {
            get { return namespaces.FirstOrDefault(n => n.Value == LeapPredicatesNamespace).Key; }
        }

        public string LeapTypesNamespace {
            get { 
                string ns = namespaces.FirstOrDefault(n => LeapTypeValues.ContainsNamespace(n.Value)).Value;

                if (string.IsNullOrEmpty(ns)) {
                    ns = LeapTypeValues.Namespaces[Version];
                }

                return ns;
            }
        }

        public string LeapTypesNamespacePrefix {
            get { return namespaces.FirstOrDefault(n => n.Value == LeapTypesNamespace).Key; }
        }

        public string LeapCategoryNamespace {
            get {
                string ns = namespaces.FirstOrDefault(n => LeapCategorySchemes.ContainsNamespace(n.Value)).Value;

                if (string.IsNullOrEmpty(ns)) {
                    ns = LeapCategorySchemes.Namespaces[Version];
                }

                return ns;
            }
        }

        private LeapVersion _version;

        public LeapVersion Version {
            get { return _version; }
            set {
                _version = value;

                if (Items.Count() == 0){
                    SetNamespaces(value);
                }
            }
        }

        private void SetNamespaces(LeapVersion value) {
            this.namespaces.Clear();

            this.namespaces.Add(LocalNamespacePrefix, LocalNamespace);
            this.namespaces.Add(LocalSchemeNamespacePrefix, LocalSchemeNamespace);
            this.namespaces[LeapCategorySchemes.NamespacePrefix] = LeapCategoryNamespace;
            this.namespaces.Add("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
            this.namespaces.Add("thr", "http://purl.org/syndication/thread/1.0");
            this.namespaces[LeapTypeValues.NamespacePrefixes[value]] = LeapTypeValues.Namespaces[value];
            this.namespaces[LeapPredicateValues.NamespacePrefixes[value]] = LeapPredicateValues.Namespaces[value];
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="LeapFeed"/> class.
        /// </summary>
        public LeapFeed() {
            this.namespaces = new Dictionary<string, string>();
            this.lookup = new Dictionary<string, LeapItem>();

            this.linkManager = new LeapLinkManager();
            this.ImportBasePath = null;

            // Add standard namespaces to feed
            this.Version = LeapVersion.v2010_07;
        }

        /// <summary>
        /// Fetches namespaces from the XML before passing the document on to SydnicationFeed.
        /// </summary>
        /// <param name="xml">The XML.</param>
        /// <returns></returns>
        protected static Dictionary<string, string> PreLoad(XmlReader xml) {
            Dictionary<string, string> namespaces = new Dictionary<string, string>();

            if (xml.IsStartElement("feed")) {
                if (xml.LocalName == "feed" && xml.HasAttributes) {
                    while (xml.MoveToNextAttribute()) {
                        if (xml.LocalName != "xmlns") {
                            namespaces.Add(xml.LocalName, xml.Value);
                        }
                    }

                    xml.IsStartElement();
                }
            }

            return namespaces;
        }

        /// <summary>
        /// Performs post load steps, repairs missing links and removes invalid links.
        /// </summary>
        protected virtual void PostLoad() {
            List<LeapLink> toRemove = new List<LeapLink>();

            foreach (LeapItem item in this.Items) {
                toRemove.Clear();

                // Recreate comment collections
                item.FixComments();

                // Repair and remove incorrect links
                foreach (LeapLink link in item.Links) {
                    if (string.IsNullOrEmpty(link.RelationshipType)) continue;

                    LeapItem otherEnd = this.LookupEntry(link.Uri);
                    LeapLinkPair linkType = this.LeapLinkManager.LookupLinkPair(link.LeapRelationshipType);

                    // Don't allow links back to the current item
                    if (item == otherEnd) otherEnd = null;

                    if (linkType != null) {
                        if (otherEnd != null) {
                            // Only try to repair standard link types to items that exist
                            bool okay = false;

                            foreach (LeapLink reverseLink in otherEnd.Links) {
                                if (reverseLink.Uri.ToString() == item.Id &&
                                    reverseLink.LeapRelationshipType == linkType.Inverse) {
                                    okay = true;
                                    break;
                                }
                            }

                            if (!okay) {
                                otherEnd.Links.Add(new LeapLink(this, linkType.Inverse, new Uri(item.Id)));
                            }
                        } else {
                            // Queue link for later removal
                            toRemove.Add(link);
                        }
                    }
                }

                foreach (LeapLink link in toRemove) {
                    // Remove links which don't point to anything
                    item.Links.Remove(link);
                }
            }
        }

        /// <summary>
        /// Loads the specified XML.
        /// </summary>
        /// <typeparam name="T">A type inherited from LeapFeed</typeparam>
        /// <param name="xml">The XML.</param>
        /// <returns></returns>
        public static new T Load<T>(XmlReader xml) where T : LeapFeed {
            return Load<T>(xml, null);
        }

        /// <summary>
        /// Loads the specified XML.
        /// </summary>
        /// <typeparam name="T">A type inherited from LeapFeed</typeparam>
        /// <param name="xml">The XML.</param>
        /// <param name="basePath">The path of the directory containing the feed to import.</param>
        /// <returns></returns>
        public static T Load<T>(XmlReader xml, string basePath) where T : LeapFeed {
            Dictionary<string, string> namespaces = PreLoad(xml);
            string error = null;

            Atom10FeedFormatter formatter = new Atom10FeedFormatter(typeof(T));
            try {
                formatter.ReadFrom(xml);
            } catch (Exception err) {
                error = err.ToString();
            }

            T feed = formatter.Feed as T;

            if (feed != null) {
                feed.ImportBasePath = basePath;
                feed.namespaces = namespaces;

                foreach (LeapItem item in feed.Items) {
                    if (!feed.lookup.ContainsKey(item.Id)) {
                        feed.lookup.Add(item.Id, item);
                    }
                }

                var ver = feed.ElementExtensions.ReadElementExtensions<string>("version", feed.LeapPredicatesNamespace).FirstOrDefault();

                switch (ver) {
                    case "http://www.leapspecs.org/2010-07/2A/":
                        feed.Version = LeapVersion.v2010_07;
                        break;
                    case "http://www.leapspecs.org/2A/":
                        feed.Version = LeapVersion.Development;
                        break;
                    default:
                        feed.Version = LeapVersion.v2009_03;
                        break;
                }

                feed.feedErrors.AddRange(LeapFeedErrorChecker.Check(feed));

                feed.PostLoad();

                if (error != null) {
                    feed.feedErrors.Add(new LeapFeedError(error));
                }

                return feed;
            }

            return null;
        }

        /// <summary>
        /// Loads a LEAP2A feed from the specified path.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <returns></returns>
        public static LeapFeed Load(string path) {
            return Load<LeapFeed>(path);
        }

        /// <summary>
        /// Loads a LEAP2A feed from the specified XmlReader.
        /// </summary>
        /// <param name="reader">The reader.</param>
        /// <returns></returns>
        public static new LeapFeed Load(XmlReader reader) {
            return Load<LeapFeed>(reader);
        }

        /// <summary>
        /// Loads a LEAP2A feed from the specified path.
        /// </summary>
        /// <typeparam name="T">A type inherited from LeapFeed</typeparam>
        /// <param name="path">The path.</param>
        /// <returns></returns>
        public static T Load<T>(string path) where T : LeapFeed {
            XmlReaderSettings settings = new XmlReaderSettings();
            settings.XmlResolver = null;
            settings.ProhibitDtd = false;
            XmlReader xtr = XmlReader.Create(path, settings);
            T feed = Load<T>(xtr, Path.GetDirectoryName(path));
            xtr.Close();

            return feed;
        }

        /// <summary>
        /// Lookups the namespace URI for a given prefix.
        /// </summary>
        /// <param name="prefix">The prefix.</param>
        /// <returns></returns>
        public string LookupNamespace(string prefix) {
            if (this.namespaces.ContainsKey(prefix)) {
                return this.namespaces[prefix];
            }

            return null;
        }

        /// <summary>
        /// Lookups the prefix for a namespace URI.
        /// </summary>
        /// <param name="nsuri">The namespace URI.</param>
        /// <returns></returns>
        public string LookupNamespacePrefix(string nsuri) {
            foreach (string prefix in this.namespaces.Keys) {
                string ns = this.namespaces[prefix];
                if (ns == nsuri) {
                    return prefix;
                }
            }

            return null;
        }

        /// <summary>
        /// Converts a CURIE to a full URI.
        /// </summary>
        /// <param name="link">The CURIE link.</param>
        /// <returns>The full URI</returns>
        public string UriFromCurie(string link) {
            if (!string.IsNullOrEmpty(link)) {
                foreach (string prefix in this.namespaces.Keys) {
                    if (link.StartsWith(prefix + ":")) {
                        string ns = NamespaceFixer.Fix(this.namespaces[prefix]);
                        return link.Replace(prefix + ":", ns);
                    }
                }
            }

            return link;
        }

        /// <summary>
        /// Converts a full URI to a CURIE.
        /// </summary>
        /// <param name="link">The full URI.</param>
        /// <returns>The CURIE link</returns>
        public string UriToCurie(string link) {
            if (!string.IsNullOrEmpty(link)) {
                foreach (string prefix in this.namespaces.Keys) {
                    string ns = NamespaceFixer.Fix(this.namespaces[prefix]);

                    if (!string.IsNullOrEmpty(ns)) {
                        if (link.StartsWith(ns)) {
                            return prefix + ":" + link.Replace(ns, "");
                        }
                    }
                }
            }

            return link;
        }

        public string UriToCurie(LeapPredicates link) {
            return LeapPredicateValues.CurieFor(link, this);
        }

        /// <summary>
        /// Lookups the Atom entry for an ID.
        /// </summary>
        /// <param name="uri">The entry URI.</param>
        /// <returns></returns>
        public LeapItem LookupEntry(Uri uri) {
            return LookupEntry(uri.ToString());
        }

        /// <summary>
        /// Lookups the Atom entry for an ID.
        /// </summary>
        /// <param name="uri">The entry URI.</param>
        /// <returns></returns>
        public LeapItem LookupEntry(string uri) {
            uri = this.UriFromCurie(uri);

            if (this.lookup.ContainsKey(uri)) {
                return this.lookup[uri];
            }

            IEnumerator<SyndicationItem> e = this.Items.GetEnumerator();

            while (e.MoveNext()) {
                LeapItem item = e.Current as LeapItem;

                if (item != null && item.Id == uri) {
                    return item;
                }
            }

            return null;
        }

        /// <summary>
        /// Creates a new <see cref="T:System.ServiceModel.Syndication.SyndicationItem"/> instance.
        /// Overrides the default SyndicationItem with a LeapItem
        /// </summary>
        /// <returns>
        /// A new <see cref="T:System.ServiceModel.Syndication.SyndicationItem"/> instance.
        /// </returns>
        protected override SyndicationItem CreateItem() {
            return new LeapItem(this);
        }

        /// <summary>
        /// Writes the attribute extensions to the specified <see cref="T:System.Xml.XmlWriter"/> 
        /// and adds LEAP2A namespaces to the feed.
        /// </summary>
        /// <param name="writer">The <see cref="T:System.Xml.XmlReader"/> to write to.</param>
        /// <param name="version">The syndication version to use while writing.</param>
        protected override void WriteAttributeExtensions(XmlWriter writer, string version) {
            foreach (string prefix in this.namespaces.Keys) {
                if (!string.IsNullOrEmpty(this.namespaces[prefix])) {
                    writer.WriteAttributeString("xmlns", prefix, null, this.namespaces[prefix]);
                }
            }

            base.WriteAttributeExtensions(writer, version);
        }

        protected override void WriteElementExtensions(XmlWriter writer, string version) {
            switch (this.Version){
                case LeapVersion.v2009_03:
                    // nop
                    break;
                case LeapVersion.v2010_07:
                    writer.WriteElementString("version", this.LeapPredicatesNamespace, "http://www.leapspecs.org/2010-07/2A/");
                    break;
                default:
                    writer.WriteElementString("version", this.LeapPredicatesNamespace, "http://www.leapspecs.org/2A/");
                    break;
            }

            var elements = new List<SyndicationElementExtension>();

            foreach (var elementExtension in this.ElementExtensions) {
                if (elementExtension.OuterName == "version"){
                    elements.Add(elementExtension);
                }
            }

            foreach (var extension in elements){
                this.ElementExtensions.Remove(extension);
            }

            base.WriteElementExtensions(writer, version);
        }
    }

    public enum LeapVersion {
        v2009_03,
        v2010_07,
        Development,
    }
}
